-----------------------------------------------------
--name       : lib/qui.lua
--description: qui - quick user interface - create UIs from strings
--author     : mpmxyz
--github page: https://github.com/mpmxyz/ocprograms
--forum page : http://oc.cil.li/index.php?/topic/
-----------------------------------------------------


local unicode = require("unicode")
local values  = require("mpm.values")
local textgfx = require("mpm.textgfx")
--local regex = require("TODO.regex") TODO: improve regex with captures

local linePattern = "([^\r\n]*)(\r?\n?)"

local qui = {
  fgColor = 0xFFFFFF,
  bgColor = 0x000000,
}
local methods = {}
local meta = {__index = methods}

--creates a new UI element using the given table
function qui.new(obj)
  checkArg(1, obj, "table", "nil")
  --obj: optional parameter
  obj = obj or {}
  --
  setmetatable(obj, meta)
  if obj.parent then
    obj:setParent(obj.parent)
  end
  obj:assertValid()
  return obj
end

--creates a user inferface from a string and a description of ui objects
function qui.load(text, uiTable, patterns)
  checkArg(1, text    , "string")
  checkArg(2, uiTable , "table")
  checkArg(3, patterns, "table", "nil")
  --patterns: optional parameter
  patterns = patterns or {
    h = "%*+(%a*)%**",
    v = "%#+(%a*)%#*",
  }
  --create parent object
  local parentObj = qui.new{
    x = 1,      y = 1,
    x_draw = 1, y_draw = 1,
    width = 0,  height = 0,
    text = text,
  }
  local function addNew(key, x, y, width, height)
    local uiContent   = uiTable[key]
    if uiContent then
      --create qui object
      local obj = qui.new{
        x = x,         y = y,
        width = width, height = height,
        parent = parentObj,
        uiKey = key,
      }
      --paste ui properties into ui object
      for k, v in pairs(uiContent) do
        local previousValue = rawget(obj, k)
        assert(previousValue == nil or previousValue == v, "Attempted to overwrite autogenerated ui property!")
        obj[k] = v
      end
      --optional init method
      if obj.init then
        obj:init()
      end
      return obj
    end
  end
  
  --objects created in the currently processed line; indexable by the x coordinate
  local thisLine = {}
  --objects created in the last processed line; indexable by the x coordinate
  local lastLine
  local lines = {}
  local nlines = 0
  
  local function checkLine(line, pattern, y, vertical)
    thisLine, lastLine = {}, thisLine
    for from, key, to in line:gmatch("()" .. pattern .. "()") do
      if to == nil then
        --no capture inside pattern: amend data
        to = key
        key = line:sub(from, to - 1)
      end
      to = to - 1
      --get position
      local x = unicode.wlen(line:sub(1, from - 1)) + 1
      local width, height = unicode.wlen(line:sub(from, to)), 1
      if key == "" then
        --this part belongs to the object above (increment its height by one)
        local aboveObject = lastLine[x]
        if aboveObject then
          if vertical then
            if aboveObject.height == width then
              aboveObject.width = aboveObject.width + 1
              thisLine[x] = aboveObject
            end
          else
            if aboveObject.width == width then
              aboveObject.height = aboveObject.height + 1
              thisLine[x] = aboveObject
            end
          end
        end
      else
        --create object
        if vertical then
          thisLine[x] = addNew(key, y, x, height, width)
        else
          thisLine[x] = addNew(key, x, y, width, height)
        end
      end
    end
  end
  
  --go through the text and extract locations of interactive objects
  for line, lineBreak in text:gmatch(linePattern) do
    if line ~= "" or lineBreak ~= "" then
      nlines = nlines + 1
      parentObj.width = math.max(parentObj.width, unicode.wlen(line))
      lines[nlines] = line
      if patterns.h then
        checkLine(line, patterns.h, nlines)
      end
    end
  end
  if patterns.v then
    thisLine = {}
    for column = 1, parentObj.width do
      local buffer = {}
      for i = 1, nlines do
        local char = unicode.sub(lines[i], column, column)
        if char == "" then
          buffer[i] = " "
        else
          buffer[i] = char
        end
      end
      local columnString = table.concat(buffer)
      checkLine(columnString, patterns.v, column, true)
    end
  end
  parentObj.height = nlines
  return parentObj
end


function methods:assertValid()
  values.checkRawNumber(self.x, "x")
  values.checkRawNumber(self.y, "y")
  values.checkRawNumber(self.width , "width")
  values.checkRawNumber(self.height, "height")
  values.checkString(self.text, "text", "")
  values.checkNumber(self.fgColor, "fgColor", "")
  values.checkNumber(self.bgColor, "bgColor", "")
  values.checkNumber(self.x_draw, "x_draw", "")
  values.checkNumber(self.y_draw, "y_draw", "")
  values.checkCallable(self.onDraw, "onDraw", "")
  values.checkCallable(self.onDraw, "onClick", "")
  values.checkCallable(self.onDraw, "onScroll", "")
end

function methods:isValid()
  return pcall(self.assertValid, self)
end

--default drawing function
function methods:onDraw(gpu, viewXmin, viewYmin, viewXmax, viewYmax)
  --get text
  if self.text == nil then
    return
  end
  local text = values.get(self.text)
  if text == nil then
    return
  end
  --check visibility
  if not textgfx.checkView(self.x_draw, self.y_draw, self.width, self.height, viewXmin, viewYmin, viewXmax, viewYmax) then
    return
  end
  
  --set color
  local foreground, background = values.get(self.fgColor or qui.fgColor), values.get(self.bgColor or qui.bgColor)
  if self.marked then
    --swapping colors when marked
    foreground, background = background, foreground
  end
  local oldForeground, oldBackground
  if foreground then
    oldForeground = gpu.setForeground(foreground)
  end
  if background then
    oldBackground = gpu.setBackground(background)
  end
  
  --draw line by line
  textgfx.draw(gpu, text, self.x_draw, self.y_draw, self.width, self.height, viewXmin, viewYmin, viewXmax, viewYmax, self.vertical)
  
  --return to old color
  if oldForeground then
    gpu.setForeground(oldForeground)
  end
  if oldBackground then
    gpu.setBackground(oldBackground)
  end
end

function methods:setParent(newParent, dontUpdate)
  --remove from old parent
  --unoptimized due to low expected number of children
  local oldParent = self.parent
  if oldParent then
    for i = 1, #oldParent do
      if oldParent[i] == self then
        table.remove(oldParent, i)
      end
    end
  end
  --change parent
  self.parent = newParent
  table.insert(newParent, self)
  --update
  if not dontUpdate then
    self:update()
  end
end
function methods:setPosition(x, y, dontUpdate)
  --change position
  self.x = x
  self.y = y
  --update
  if not dontUpdate then
    self:update()
  end
end

function methods:isInBox(x, y)
  --transform to relative coordinates
  x = x - self.x_draw
  y = y - self.y_draw
  --return result
  return (x >= 0 and y >= 0 and x < self.width_draw and y < self.height_draw)
end

local function newChildIterator(name, reversed)
  if reversed then
    return function(self, ...)
      --reversed iteration
      for i = #self, 1, -1 do
        local obj = self[i]
        local abort = obj[name](obj, ...)
        if abort ~= nil then
          return abort
        end
      end
    end
  else
    return function(self, ...)
      --normal iteration
      for i = 1, #self do
        local obj = self[i]
        local abort = obj[name](obj, ...)
        if abort ~= nil then
          return abort
        end
      end
    end
  end
end
--updates the drawing positions
methods.updateChildren = newChildIterator("update")
function methods:update()
  if self.preUpdate then
    --e.g. to run ui layout managers
    self:preUpdate()
  end
  --updating the position and shape of this object
  local parent = self.parent
  if parent then
    self.x_draw = self.x + parent.x_draw - 1
    self.y_draw = self.y + parent.y_draw - 1
  else
    self.x_draw = self.x
    self.y_draw = self.y
  end
  self.width_draw  = self.width
  self.height_draw = self.height
  if self.onUpdate then
    --e.g. for graphics updates
    self:onUpdate()
  end
  return self:updateChildren()
end
--draws all gadgets
methods.drawChildren = newChildIterator("draw")
function methods:draw(gpu)
  checkArg(1, gpu, "table")
  --remember gpu for redraw actions
  self.lastGPU = gpu
  --call drawing function
  self:onDraw(gpu)
  --recursion on children
  return self:drawChildren(gpu)
end
--redraws all gadgets using the last used gpu
methods.redrawChildren = newChildIterator("redraw")
function methods:redraw(...)
  if self.lastGPU then
    --call drawing function
    self:onDraw(self.lastGPU, ...)
    --recursion on children
    return self:redrawChildren(...)
  end
end

local function newEventProcessor(childAction, eventAction)
  return function(self, x, y, ...)
    if self.disabled then
      return
    end
    local childResult = self[childAction](self, x, y, ...)
    if childResult ~= nil then
      return childResult
    end
    if self[eventAction] and self:isInBox(x, y) then
      self[eventAction](self, x, y, ...)
      return true
    end
  end
end

  
function methods:isInteractive()
  return (self.onClick ~= nil or self.onScroll ~= nil)
end

--checks for an action at the given position and executes it
methods.clickChildren = newChildIterator("click", true)
methods.click = newEventProcessor("clickChildren", "onClick")
--checks for an action at the given position and executes it
methods.scrollChildren = newChildIterator("scroll", true)
methods.scroll = newEventProcessor("scrollChildren", "onScroll")

return qui
